import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 原生java调用(除开JSON)
 * 线程不安全
 *
 * @author holate
 */
public class Holiday {
    /**
     * 网址前缀
     */
    private static final String BASE_PREFIX = "https://gitee.com/holate/public-holiday/raw/master/holidays/year/";
    /**
     * 网址后缀
     */
    private static final String BASE_SUFFIX = ".json";
    /**
     * 使用读写锁(单线程忽略)
     */
    private static final ReadWriteLock READ_WRITE_LOCK = new ReentrantReadWriteLock();
    /**
     * 法律规定的放假日期，30+366*2/7向上取整
     */
    private static final Set<String> LAW_HOLIDAYS = new HashSet<>(135);

    /**
     * 由于放假需要额外工作的周末，366*5/7向上取整
     */
    private static final Set<String> EXTRA_WORKDAYS = new HashSet<>(262);
    /**
     * 调用接口获取过的年份,大小设置为4年
     */
    private static final Set<Integer> ALREADY_OBTAIN = new HashSet<>(4);
    /**
     * 格式化日期到天
     */
    private static final String TO_DAY = "yyyy-MM-dd";
    /**
     * 调用接口每天调用一次，记录上一次调用日期
     */
    private static String initDate = "";

    /**
     * 返回指定日期后{@param days}个工作日的日期
     *
     * @param startDate 开始日期
     * @param days      工作日数
     * @return {@link Date}
     * @date 2020/10/26
     * @author holate
     */
    public static Date calculateWorkDay(Date startDate, int days) {
        // 每日清理节假日重新从接口获取
        cleanDate();
        if (startDate == null) {
            return null;
        }
        READ_WRITE_LOCK.readLock().lock();
        try {
            Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT+8"));
            cal.setTime(startDate);
            //累加自然日
            for (int i = 1; i <= days; i++) {
                cal.add(Calendar.DATE, 1);
                Integer year = cal.get(Calendar.YEAR);
                //如果那一年的数据没有获取过，则获取数据
                if (!ALREADY_OBTAIN.contains(year)) {
                    getHolidayForMonth(year);
                }
                //如果是假日，延后一天
                if (isHoliday(cal)) {
                    days++;
                }
            }
            return cal.getTime();
        } finally {
            READ_WRITE_LOCK.readLock().unlock();
        }
    }

    /**
     * 返回指定日期后{@param days}个自然日的日期
     *
     * @param startDate 开始日期
     * @param days      自然日数
     * @return {@link Date}
     * @date 2020/10/26
     * @author holate
     */
    public static Date calculateNormalDay(Date startDate, int days) {
        if (startDate == null) {
            return null;
        }
        Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT+8"));
        cal.setTime(startDate);
        cal.add(Calendar.DATE, days);
        return cal.getTime();
    }

    /**
     * 返回指定日期后{@param days}个节假日的日期
     *
     * @param startDate 开始日期
     * @param days      自然日数
     * @return {@link Date}
     * @date 2020/10/26
     * @author holate
     */
    public static Date calculateNextHoliday(Date startDate, int days) {
        // 每日清理节假日重新从接口获取
        cleanDate();
        if (startDate == null) {
            return null;
        }
        READ_WRITE_LOCK.readLock().lock();
        try {
            Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT+8"));
            cal.setTime(startDate);
            //累加自然日
            for (int i = 1; i <= days; i++) {
                cal.add(Calendar.DATE, 1);
                Integer year = cal.get(Calendar.YEAR);
                //如果那一年的数据没有获取过，则获取数据
                if (!ALREADY_OBTAIN.contains(year)) {
                    getHolidayForMonth(year);
                }
                //如果是假日，延后一天
                if (!isHoliday(cal)) {
                    days++;
                }
            }
            return cal.getTime();
        } finally {
            READ_WRITE_LOCK.readLock().unlock();
        }
    }

    /**
     * 判断一天是不是工作日
     *
     * @param date 日期
     * @return true:休息日，false:工作日
     * @date 2020/10/26
     */
    public static boolean isHoliday(Date date) {
        cleanDate();
        Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT+8"));
        cal.setTime(date);
        Integer year = cal.get(Calendar.YEAR);
        if (!ALREADY_OBTAIN.contains(year)) {
            getHolidayForMonth(year);
        }
        return isHoliday(cal);
    }

    /**
     * 判断一天是不是休息日
     *
     * @param cal 日期
     * @return true:休息日，false:工作日
     * @date 2020/10/26
     */
    private static boolean isHoliday(Calendar cal) {
        SimpleDateFormat simpleFormat = new SimpleDateFormat(TO_DAY);
        String day = simpleFormat.format(cal.getTime());
        // 法定节假日必定是休息日
        if (LAW_HOLIDAYS.contains(day)) {
            return true;
        }
        // 排除法定节假日外的非周末必定是工作日
        if (!isWeekend(cal)) {
            return false;
        }
        // 所有周末中只有非补班的才是休息日
        return !EXTRA_WORKDAYS.contains(day);
    }

    /**
     * 判断是否为周末
     *
     * @param cal 日期
     * @return true:是周末，false:不是周末
     * @date 2020/10/26
     * @author holate
     */
    private static boolean isWeekend(Calendar cal) {
        return cal.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY || cal.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY;
    }

    /**
     * 每天清空日期
     *
     * @date 2020/10/26
     * @author holate
     */
    private static void cleanDate() {
        SimpleDateFormat simpleFormat = new SimpleDateFormat(TO_DAY);
        String day = simpleFormat.format(new Date());
        //如果初始化时间不为当天
        if (!day.equals(initDate)) {
            READ_WRITE_LOCK.writeLock().lock();
            try {
                if (!day.equals(initDate)) {
                    ALREADY_OBTAIN.clear();
                    EXTRA_WORKDAYS.clear();
                    LAW_HOLIDAYS.clear();
                    initDate = day;
                }
            } finally {
                READ_WRITE_LOCK.writeLock().unlock();
            }
        }
    }

    /**
     * 通过年月查询第三方节假日，获取节日和调休的工作日
     *
     * @param year 年份
     * @return {@link Set<Calendar>}
     * @date 2020/10/26
     * @author holate
     */
    @SuppressWarnings("unchecked")
    private static void getHolidayForMonth(Integer year) {
        //json对象，获取值使用的键
        String holiday = "holiday";
        //访问url
        String url = BASE_PREFIX + year + BASE_SUFFIX;
        String json = getUrl(url);
        if (json.length() == 0) {
            return;
        }
        //构建json对象
        JSONObject body = JSON.parseObject(json);
        Map<String, JSONObject> days = (Map<String, JSONObject>) body.get(holiday);
        for (Map.Entry<String, JSONObject> entry : days.entrySet()) {
            Boolean isHoliday = (Boolean) entry.getValue().get(holiday);
            if (isHoliday) {
                LAW_HOLIDAYS.add(year + "-" + entry.getKey());
            } else {
                EXTRA_WORKDAYS.add(year + "-" + entry.getKey());
            }
        }
        ALREADY_OBTAIN.add(year);
    }

    /**
     * 使用get请求访问url
     *
     * @param url 网址
     * @return {@link String} 返回响应内容
     * @date 2020/10/26
     * @author holate
     */
    private static String getUrl(String url) {
        //访问返回结果
        StringBuilder result = new StringBuilder();
        //读取访问结果
        BufferedReader read = null;
        try {
            //创建url
            URL realUrl = new URL(url);
            //打开连接
            URLConnection connection = realUrl.openConnection();
            // 设置通用的请求属性
            connection.setRequestProperty("accept", "*/*");
            connection.setRequestProperty("connection", "Keep-Alive");
            connection.setRequestProperty("user-agent", "Mozilla/5.0");
            //建立连接
            connection.connect();
            // 定义 BufferedReader输入流来读取URL的响应
            read = new BufferedReader(new InputStreamReader(
                connection.getInputStream(), StandardCharsets.UTF_8));
            String line;//循环读取
            while ((line = read.readLine()) != null) {
                result.append(line).append("\n");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            closeReader(read);
        }
        return result.toString();
    }

    /**
     * 关闭读取字符流
     *
     * @param reader 读取字符流
     * @date 2020/10/26
     * @author holate
     */
    private static void closeReader(Reader reader) {
        if (reader != null) {
            try {
                reader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}